package com.dyx.source.nio;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * 对比 select/poll 和 epoll 我们发现epoll效率更高。
 * 如果 select/poll 中注册了大量的 channel，就要不停的轮询每个channel，来判断那些channel已经就绪。而 epoll 则不需要轮询。
 *
 * jdk1.4 是使用的 select/poll 模型
 * jdk1.5 以后把select/poll 改为了epoll模型
 *
 * 使用 nio 实现的通信-服务端代码 (channel + buffer + selector , selector 是nio 网络编程的核心)
 * https://www.cnblogs.com/fatmanhappycode/p/12345391.html
 *
 *
 * 弱弱的说下，菜的抠脚的我在学习NIO和Reactor的时候，有一个问题是百思不得其解：不是说NIO很强大吗，在不开启的线程的时候，
 * 一个服务端可以同时处理多个客户端吗？为什么这里又说只有处理完一个请求，才能处理下一个请求。不知道是否有人和我一个想法，
 *
 * NIO在不开启线程的时候，一个服务端可以同时处理多个客户端，是指的一个客户端可以监听多个客户端的连接、读写事件，
 * 真正做业务处理还是“一夫当关，万夫莫开”的效果。
 *
 * 这个NioServer是Reactor单线程版模型的一种体现
 *
 * selector实现机制：
 * https://www.iteye.com/blog/goon-1764057
 * https://www.iteye.com/blog/goon-1764067
 * https://www.iteye.com/blog/goon-1775421
 *
 * @author dengyouxu
 * @description TODO
 * @since 2021-05-04 下午2:26
 */
public class NioServer {

    public static void main(String[] args) throws Exception {
        System.out.println("NIO Server starting...");

        // 1. 开启一个服务端的通道
        final ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

        // 2. 将这个通道的模式设置为非阻塞
        serverSocketChannel.configureBlocking(false);

        // 3. 将该通道与连接的9999端口绑定起来
        serverSocketChannel.bind(new InetSocketAddress(9999));

        // 4. 获取选择器
        final Selector selector = Selector.open();
        System.out.println("OP_ACCEPT 事件之前");
        // 5. 将这个通道注册到该选择器上，并指定这个选择器开启监听 接收连接事件（适用于客户端要连接到服务端这个事件）,
        // 当一个客户端连接到来时，OP_ACCEPT事件就绪
        // SelectionKey.OP_ACCEPT —— 接收连接继续事件，表示服务器监听到了客户连接，服务器可以接收这个连接了
        // SelectionKey.OP_CONNECT —— 连接就绪事件，表示客户与服务器的连接已经建立成功
        // SelectionKey.OP_READ —— 读就绪事件，表示通道中已经有了可读的数据，可以执行读操作了（通道目前有数据，可以进行读操作了）
        // SelectionKey.OP_WRITE —— 写就绪事件，表示已经可以向通道写数据了（通道目前可以用于写操作）

        // 与Selector一起使用时，SelectableChannel必须处于非阻塞模式下。
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("注册OP_ACCEPT事件之后一直阻塞,此时的selector-->");
        printSelectorInfo(selector);
        // 6. 一旦上面的服务端通道注册好了，我们就可以遍历 选择器 上已经就绪的事件，进行处理，调用selector的select()方法，它是个阻塞方法，
        // 如果没有事件就绪会一直阻塞，直到有事件就绪了，它才开始运行

        //
        while (selector.select() > 0) {
            // select方法会一直阻塞，直到在selector上已注册的(SelectableChannel对象实例)有就绪事件发生了，才会唤醒这个阻塞方法...
            System.out.println("有就绪事件了...");
            final Set<SelectionKey> keys = selector.keys();
            System.out.println("keys-->" + keys);
            final Set<SelectionKey> selectedKeys = selector.selectedKeys();
            System.out.println("selectedKeys-->" + selectedKeys);
            // 7. selector.select() > 0说明，有事件就绪了，调用selector.selectedKeys()方法获取到这些已就绪的事件
            final Iterator<SelectionKey> selectionKeyIterator = selectedKeys.iterator();
            // 8. 遍历这些已准备好的事件
            while (selectionKeyIterator.hasNext()) {
                // 9. 对这些事件进行判断，判断这个事件是什么，
                final SelectionKey selectionKey = selectionKeyIterator.next();
                System.out.println("当前的SelectionKey-->" + selectionKey);
                System.out.println("当前的Selection-->" + selectionKey.interestOps());
                // 操作完毕之后需要将这个事件，从迭代器之中移除掉(这里会从selector.selectedKeys()中移除)，否则的话，下次遍历时，这个事件还在，会重复处理了
                selectionKeyIterator.remove();
                System.out.println("在移除之后的selector:");
                printSelectorInfo(selector);

                // 9.1 如果这个事件是 接收事件，说明外部有新的客户端连接进来了
                if (selectionKey.isAcceptable()) {
                    System.out.println("selectionKey-->" + selectionKey + " is acceptable");
                    // 9.2 在服务端代码中，用一个SocketChannel来代表已连接到服务端的客户端连接通道，accept本身是阻塞的，但前面已经判断是来连接的请求
                    final SocketChannel socketChannel = serverSocketChannel.accept();
                    System.out.println("serverSocketChannel 接受后产生的SocketChannel-->" + socketChannel + ",hashcode-->" + System.identityHashCode(socketChannel));
                    // 9.3 需要将客户端通道设置为非阻塞模式
                    socketChannel.configureBlocking(false);
                    //在这里可以给客户端发送信息哦
                    socketChannel.write(ByteBuffer.wrap(new String("哥们你好\n").getBytes()));
                    // 9.4 将这个客户端也要注册到选择器上，注册的事件为 读事件，
                    // 这里想要选择器来监听客户端通道上的读取事件，这里为啥是read事件呢？
                    socketChannel.register(selector, SelectionKey.OP_READ);

                    System.out.println("在acceptable之后的selector的key为");
                    printSelectorInfo(selector);
                } else if (selectionKey.isReadable()) {
                    // 10 如果这个事件是一个读取事件的话，就是上面9.4 步骤执行完之后，又会进入到selector.select() > 0的逻辑，但是这次的事件是读取事件...
                    System.out.println("selectionKey is readable");
                    // 10.1 获取读就绪状态的通道
                    final SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
                    System.out.println("selectionKey channel()--->" + socketChannel + ",hashcode-->" + System.identityHashCode(socketChannel));
                    // 10.2 读取数据，预先分配一个buffer缓冲区
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    int len = 0;
                    // 需要注意的是：
                    // 当我们telnet切断客户端链接时，会一直出发SocketChannel读就绪事件。会出现循环读问题。或者客户端断开执行完毕断开连接时
                    // “当客户端的链接异常断开，此时代表这个链接的channel一直处于readable的状态，如何检查链接已断开呢？
                    // 一段时间的试验发现，链接断开后，虽然该channel的ready operation是OP_READ，但是此时channel.read(buffer)返回-1，此时可以增加一个判断”。
                    try {
                        System.out.println("socketChannel-->" + socketChannel + ",hashcode-->" + System.identityHashCode(socketChannel) + " is still Connected?" + socketChannel.isConnected());
                        // 10.3 将socketChannel中的数据读取到缓冲区当中
                        while ((len = socketChannel.read(buffer)) > 0) {
                            System.out.println("读取数据的长度为len-->" + len);
                            // 10.4 转换为读模式
                            buffer.flip();
                            byte[] bytes = new byte[buffer.limit() - buffer.position()];
                            buffer.get(bytes);
                            System.out.print("byte data is -->" + Arrays.toString(bytes)
                                    + ",byte to string data is -->" + new String(bytes));
                            // 10.5 循环读取的情况下，需要将buffer清空，否则会有数据的重复
                            buffer.clear();
                        }

                        // 其实read返回0有3种情况：
                        // 一是某一时刻socketChannel中当前（注意是当前）没有数据可以读，这时会返回0，
                        // 其次是bytebuffer的position等于limit了，即bytebuffer的remaining等于0，这个时候也会返回0，
                        // 最后一种情况就是客户端的数据发送完毕了（注意看后面的程序里有这样子的代码），
                        // 这个时候客户端想获取服务端的反馈调用了recv函数，若服务端继续read，这个时候就会返回0。
                        if (len == 0) {
                            System.out.println("读取数据的长度为len-->" + len);
                        }

                        // read返回-1说明客户端的数据发送完毕，并且主动的close socket。
                        // 所以在这种场景下，（服务器程序）你需要关闭socketChannel并且取消key，最好是退出当前函数。
                        // 注意，这个时候服务端要是继续使用该socketChannel进行读操作的话，就会抛出“远程主机强迫关闭一个现有的连接”的IO异常。
                        if (len < 0) {
                            System.out.println("读取数据的长度为len-->" + len);
                            System.out.println("socketChannel" + socketChannel + "closed");
                            socketChannel.close();
                            selectionKey.cancel();
                        }
                    } catch (Exception ex) {
                        System.out.println("socketChannel" + socketChannel + "closed");
                        socketChannel.close();
                        selectionKey.cancel();
                    }
                }

                TimeUnit.SECONDS.sleep(5);
            }
        }
    }

    private static void printSelectorInfo(Selector selector) {
        final Set<SelectionKey> keys = selector.keys();
        final Set<SelectionKey> selectionKeys = selector.selectedKeys();
        System.out.println("selector.keys()-->" + keys);
        System.out.println("selector.selectedKeys()-->" + selectionKeys);
    }
}
